#!/usr/bin/env python3
# -*-coding:utf-8-*-
#
# DX Library Portfolio
# derivatives_position.py
#
import numpy as np
import pandas as pd
from geometric_brownian_motion import geometric_brownian_motion
from jump_diffusion import jump_diffusion
from square_root_diffusion import square_root_diffusion
from valuation_mcs_european import valuation_mcs_european
from valuation_mcs_american import valuation_mcs_american
from sn_random_numbers import sn_random_numbers
from simulation_class import simulation_class
from market_environment import market_environment

class derivatives_position(object):
    """
    Class to model a derivatives position.
    Attributes
    ==========
    name : string
    name of the object
    quantity : float
    number of assets/derivatives making up the position
    underlying : string
    name of asset/risk factor for the derivative
    mar_env : instance of market_environment
    constants, lists, and curves relevant for valuation_class
    otype : string
    valuation class to use
    payoff_func : string
    payoff string for the derivative
    Methods
    =======
    get_info :
    prints information about the derivative position
    """
    def __init__(self, name, quantity, underlying, mar_env, otype, payoff_func):
        self.name = name
        self.quantity = quantity
        self.underlying = underlying
        self.mar_env = mar_env
        self.otype = otype
        self.payoff_func = payoff_func
        
    def get_info(self):
        print("NAME")
        print(self.name, '\n')
        print ("QUANTITY")
        print (self.quantity, '\n')
        print ("UNDERLYING")
        print (self.underlying, '\n')
        print ("MARKET ENVIRONMENT")
        print ("\n**Constants**")
        for key, value in self.mar_env.constants.items():
            print (key, value)
        print ("\n**Lists**")
        for key, value in self.mar_env.lists.items():
            print( key, value)
        print ("\n**Curves**")
        for key in self.mar_env.curves.items():
            print (key, value)
        print ("\nOPTION TYPE")
        print (self.otype, '\n')
        print ("PAYOFF FUNCTION")
        print (self.payoff_func)


# models available for risk factor modeling
models = {'gbm' : geometric_brownian_motion,
          'jd' : jump_diffusion,
          'srd' : square_root_diffusion}
# allowed exercise types
otypes = {'European' : valuation_mcs_european,
          'American' : valuation_mcs_american}
class derivatives_portfolio(object):
    """ Class for building portfolios of derivatives positions.
    Attributes
    ==========
    name : str
    name of the object
    positions : dict
    dictionary of positions (instances of derivatives_position class)
    val_env : market_environment
    market environment for the valuation
    assets : dict
    dictionary of market environments for the assets
    correlations : list
    correlations between assets
    fixed_seed : Boolean
    flag for fixed rng seed
    Methods
    =======
    get_positions :
    prints information about the single portfolio positions
    get_statistics :
    returns a pandas DataFrame object with portfolio statistics
    """
    def __init__(self, name, positions, val_env, assets,
                 correlations=None, fixed_seed=False):
        self.name = name
        self.positions = positions
        self.val_env = val_env
        self.assets = assets
        self.underlyings = set()
        self.correlations = correlations
        self.time_grid = None
        self.underlying_objects = {}
        self.valuation_objects = {}
        self.fixed_seed = fixed_seed
        self.special_dates = []
        for pos in self.positions:
            # determine earliest starting_date
            self.val_env.constants['starting_date'] = \
                                                      min(self.val_env.constants['starting_date'],
                                                          positions[pos].mar_env.pricing_date)
            # determine latest date of relevance
            self.val_env.constants['final_date'] = \
                                                   max(self.val_env.constants['final_date'],
                                                       positions[pos].mar_env.constants['maturity'])
            # collect all underlyings
            # add to set; avoids redundancy
            self.underlyings.add(positions[pos].underlying)
        # generate general time grid
        start = self.val_env.constants['starting_date']
        end = self.val_env.constants['final_date']
        time_grid = pd.date_range(start=start,end=end,
                                  freq=self.val_env.constants['frequency']
        ).to_pydatetime()
        time_grid = list(time_grid)
        for pos in self.positions:
            maturity_date = positions[pos].mar_env.constants['maturity']
            if maturity_date not in time_grid:
                time_grid.insert(0, maturity_date)
                self.special_dates.append(maturity_date)
            if start not in time_grid:
                time_grid.insert(0, start)
            if end not in time_grid:
                time_grid.append(end)
            # delete duplicate entries
            time_grid = list(set(time_grid))
            # sort dates in time_grid
            time_grid.sort()
            self.time_grid = np.array(time_grid)
            self.val_env.add_list('time_grid', self.time_grid)
            
            if correlations is not None:
                # take care of correlations
                ul_list = sorted(self.underlyings)
                correlation_matrix = np.zeros((len(ul_list), len(ul_list)))
                np.fill_diagonal(correlation_matrix, 1.0)
                correlation_matrix = pd.DataFrame(correlation_matrix,
                                                  index=ul_list, columns=ul_list)
                for i, j, corr in correlations:
                    corr = min(corr, 0.999999999999)
                    # fill correlation matrix
                    correlation_matrix.loc[i, j] = corr
                    correlation_matrix.loc[j, i] = corr
                # determine Cholesky matrix
                cholesky_matrix = np.linalg.cholesky(np.array(correlation_matrix))
                # dictionary with index positions for the
                # slice of the random number array to be used by
                # respective underlying
                rn_set = {asset: ul_list.index(asset)
                          for asset in self.underlyings}
                # random numbers array, to be used by
                # all underlyings (if correlations exist)
                random_numbers = sn_random_numbers((len(rn_set),
                                                    len(self.time_grid),
                                                    self.val_env.constants['paths']),
                                                   fixed_seed=self.fixed_seed)
                # add all to valuation environment that is
                # to be shared with every underlying
                self.val_env.add_list('cholesky_matrix', cholesky_matrix)
                self.val_env.add_list('random_numbers', random_numbers)
                self.val_env.add_list('rn_set', rn_set)
                
                for asset in self.underlyings:
                    # select market environment of asset
                    mar_env = self.assets[asset]
                    # add valuation environment to market environment
                    mar_env.add_environment(val_env)
                    # select right simulation class
                    model = models[mar_env.constants['model']]
                    # instantiate simulation object
                    if correlations is not None:
                        self.underlying_objects[asset] = model(asset, mar_env,
                                                               corr=True)
                    else:
                        self.underlying_objects[asset] = model(asset, mar_env,
                                                               corr=False)
                for pos in positions:
                    # select right valuation class (European, American)
                    val_class = otypes[positions[pos].otype]
                    # pick market environment and add valuation environment
                    mar_env = positions[pos].mar_env
                    mar_env.add_environment(self.val_env)
                    # instantiate valuation class
                    self.valuation_objects[pos] = \
                                                  val_class(name=positions[pos].name,
                                                            mar_env=mar_env,
                                                            underlying=self.underlying_objects[
                                                                positions[pos].underlying],
                                                            payoff_func=positions[pos].payoff_func)
                    
    def get_positions(self):
        """ Convenience method to get information about
        all derivatives positions in a portfolio. """
        for pos in self.positions:
            bar = '\n' + 50 * '-'
            print (bar)
            self.positions[pos].get_info()
            print (bar)
            
    def get_statistics(self, fixed_seed=False):
        """ Provides portfolio statistics. """
        res_list = []
        # iterate over all positions in portfolio
        for pos, value in self.valuation_objects.items():
            p = self.positions[pos]
            pv = value.present_value(fixed_seed=fixed_seed)
            res_list.append([
                p.name,
                p.quantity,
                # calculate all present values for the single instruments
                pv,
                value.currency,
                # single instrument value times quantity
                pv * p.quantity,
                # calculate Delta of position
                value.delta() * p.quantity,
                # calculate Vega of position
                value.vega() * p.quantity,
            ])
            # generate a pandas DataFrame object with all results
            res_df = pd.DataFrame(res_list,
                                  columns=['name', 'quant.', 'value', 'curr.',
                                           'pos_value', 'pos_delta', 'pos_vega'])
            return res_df
